/*
 * $Id: LoggerCompiler.java 114 2013-01-16 07:33:27Z vladimirow@mail.ru $
 * $HeadURL: http://ultralog.googlecode.com/svn/ultralog/tags/release-0.1/src/java/com/google/code/ultralog/LoggerCompiler.java $
 *
 * This file is a part of ultralog project (http://code.google.com/p/ultralog/).
 * Released under New BSD License.
 */
package com.google.code.ultralog;

import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Pattern;

import com.google.code.ultralog.factory.LoggerClassGenerator;
import com.google.code.ultralog.factory.LoggerInterfaceUtils;

/**
 * Compiles logger interfaces into implementation classes.
 *
 * @author Mikhail Vladimirov
 */
public class LoggerCompiler
{
    private final ClassLoader classLoader;
    private final File destination;

    /**
     * Create new compiler that uses given class loader and writes
     * compiled logger classes into given destination directory.
     *
     * @param classLoader class loader to use
     * @param destination destination directory to write compiled logger
     *        classes to
     */
    public LoggerCompiler (ClassLoader classLoader, File destination)
    {
        if (classLoader == null)
            throw new IllegalArgumentException ("Class loader is null");

        if (destination == null)
            throw new IllegalArgumentException ("Destination is null");

        this.classLoader = classLoader;
        this.destination = destination;
    }

    /**
     * Compile logger interface with given name.
     *
     * @param name name of the logger interface or logger interface class file
     * @throws Exception if some error occurred during compilation
     */
    @SuppressWarnings("unchecked")
    public void compile (String name) throws Exception
    {
        if (name == null)
            throw new IllegalArgumentException ("Name is null");

        System.out.println (name);

        Class <?> loggerInterface = null;

        try
        {
            loggerInterface = classLoader.loadClass (name);
        }
        catch (ClassNotFoundException ex)
        {
            // Ignore
        }

        if (loggerInterface == null)
        {
            FileInputStream input = new FileInputStream (name);
            try
            {
                loggerInterface =
                    new MyClassLoader (classLoader).loadClass (input);
            }
            finally
            {
                input.close ();
            }
        }

        if (!Logger.class.isAssignableFrom (loggerInterface))
            throw new IllegalArgumentException (
                "Logger interface (" + loggerInterface + ") does not extend " +
                Logger.class.getName ());

        String loggerInterfaceName = loggerInterface.getName ();
        String loggerClassName = loggerInterfaceName + "$Impl";

        byte [] classBody = new LoggerClassGenerator (
            LoggerInterfaceUtils.
                describeLoggerInterface (
                    (Class <? extends Logger>)loggerInterface)).
                    generateLoggerClass (
                        loggerInterfaceName,
                        loggerClassName);

        String [] parts = loggerClassName.split ("\\.", -1);
        parts [parts.length - 1] += ".class";

        File file = destination;
        for (String part: parts)
            file = new File (file, part);

        File parentFile = file.getParentFile ();
        if (parentFile != null)
            parentFile.mkdirs ();

        FileOutputStream output = new FileOutputStream (file);
        try
        {
            output.write (classBody);
        }
        finally
        {
            output.close ();
        }
    }

    /**
     * Logger compiler entry point.
     *
     * @param args command-line arguments
     * @throws Exception if some error occurred during compilation
     */
    public static void main (String[] args) throws Exception
    {
        if (args == null)
            throw new IllegalArgumentException ("Args is null");

        int argsLength = args.length;

        if (argsLength == 0)
            usage ();
        else
        {
            String classpath = null;
            String dir = null;
            List <String> interfaces = new ArrayList <String> ();

            int state = 0;
            for (int i = 0; i < argsLength; i++)
            {
                String arg = args [i];

                switch (state)
                {
                case 0: // Initial
                    if ("-classpath".equals (arg))
                        state = 1;
                    else if ("-d".equals (arg))
                        state = 2;
                    else if ("--".equals (arg))
                        state = 3;
                    else if (arg.startsWith ("-"))
                        throw new IllegalArgumentException (
                            "Unknown command-line option: " + arg);
                    else
                    {
                        interfaces.add (arg);
                        state = 3;
                    }
                    break;
                case 1: // After `-classpath' option
                    if (classpath != null)
                        throw new IllegalArgumentException (
                            "Duplicate command-line option: -classpath");

                    classpath = arg;
                    state = 0;
                    break;
                case 2: // After `-d' option
                    if (dir != null)
                        throw new IllegalArgumentException (
                            "Duplicate command-line option: -dir");

                    dir = arg;
                    state = 0;
                    break;
                case 3: // Interfaces
                    interfaces.add (arg);
                    break;
                default:
                    throw new Error ("Unknown state: " + state);
                }
            }

            if (classpath == null)
                classpath = "";

            if (dir == null)
                dir = System.getProperty ("user.dir", ".");

            String [] parts = classpath.split (
                Pattern.quote (
                    System.getProperty ("path.separator")));

            int partsCount = parts.length;
            URL [] urls = new URL [partsCount];
            for (int i = 0; i < partsCount; i++)
                urls [i] = new File (parts [i]).toURI ().toURL ();

            URLClassLoader classLoader = new URLClassLoader (
                urls, LoggerCompiler.class.getClassLoader ());

            LoggerCompiler compiler =
                new LoggerCompiler (classLoader, new File (dir));

            for (String name: interfaces)
            {
                try
                {
                    compiler.compile (name);
                }
                catch (Throwable ex)
                {
                    ex.printStackTrace ();
                }
            }
        }
    }

    private static void usage ()
    {
        System.out.println ("Usage:");
        System.out.println (
            "\tLoggerCompiler [ -classpath <classpath> ] [ -d <dir> ] <interface> ...");
        System.out.println ();
        System.out.println ("Compile logger interfaces into implementation classes.");
        System.out.println ("Logging interfaces to be compiled could be specified as class names or class files names.");
        System.out.println ();
        System.out.println ("Options:");
        System.out.println ("\t-classpath <classpath>");
        System.out.println ("\t\tClasspath to be used when loading logger interfaces (default: empty).");
        System.out.println ("\t-d <dir>");
        System.out.println ("\t\tDestination directory for generated class files (default: current directory).");
    }

    private static class MyClassLoader extends ClassLoader
    {
        public MyClassLoader (ClassLoader parent)
        {
            super (parent);
        }

        public Class <?> loadClass (InputStream input)
            throws IOException
        {
            ByteArrayOutputStream output = new ByteArrayOutputStream ();

            byte [] buffer = new byte [65536];
            int l;
            while ((l = input.read (buffer)) > 0)
            {
                output.write (buffer, 0, l);
            }

            byte [] classBody = output.toByteArray ();

            return defineClass (null, classBody, 0, classBody.length);
        }
    }
}
